﻿using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
using System.Xml.Serialization;
using JetBrains.Annotations;
using MugenMvvmToolkit.Infrastructure;
using MugenMvvmToolkit.Interfaces;
using MugenMvvmToolkit.Models;
using MugenMvvmToolkit.Utils;

namespace MugenMvvmToolkit.ViewModels
{
    /// <summary>
    ///     Represents the base class which adds support <see cref="INotifyPropertyChanged" />.
    /// </summary>
#if WPF
    [Serializable]
#endif
    [DataContract(IsReference = true)]
    public abstract class NotifyPropertyChangedBase : INotifyPropertyChanged, ISuspendNotifications
    {
        #region Fields

#if WPF
        [NonSerialized]
#endif
        [XmlIgnore]
        private IThreadManager _threadManager;

#if WPF
        [NonSerialized]
#endif
        [XmlIgnore]
        private int _suspendCount;

#if WPF
        [NonSerialized]
#endif
        [XmlIgnore]
        private bool _isNotificationsDirty;

        #endregion

        #region Constructors

        /// <summary>
        ///     Initializes a new instance of the <see cref="NotifyPropertyChangedBase" /> class.
        /// </summary>        
        protected NotifyPropertyChangedBase()
        {
            // ReSharper disable DoNotCallOverridableMethodsInConstructor
            PropertyChangeExecutionMode = GlobalSettings.PropertyChangeExecutionMode;
        }

        #endregion

        #region Implementation of INotifyPropertyChanged

        /// <summary>
        ///     Occurs when a property value changes.
        /// </summary>
#if WPF
        [field: NonSerialized]
#endif
        [field: XmlIgnore]
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the value that indicates that model uses the GlobalSettings.DefaultThreadManager.
        /// </summary>
        internal bool IsDefaultThreadManager
        {
            get { return _threadManager == null; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether change to the collection is made when its notifications are suspended.
        /// </summary>
        protected bool IsNotificationsDirty
        {
            get { return _isNotificationsDirty; }
            set { _isNotificationsDirty = value; }
        }

        /// <summary>
        ///     Gets or sets the <see cref="IThreadManager" />. By default used <see cref="GlobalSettings.DefaultThreadManager" />.
        /// </summary>
        [XmlIgnore]
        protected internal IThreadManager ThreadManager
        {
            get
            {
                if (_threadManager == null)
                    return GlobalSettings.DefaultThreadManager;
                return _threadManager;
            }
            set { _threadManager = value; }
        }

        /// <summary>
        /// Specifies the execution mode for raise property change event.
        /// </summary>
        protected internal virtual ExecutionType PropertyChangeExecutionMode { get; set; }

        #endregion

        #region Methods

        /// <summary>
        ///     Calls the event for the specified property.
        /// </summary>
        [NotifyPropertyChangedInvocator("propName")]
        protected internal void OnPropertyChanged([CallerMemberName] string propName = "")
        {
            OnPropertyChanged(propName, PropertyChangeExecutionMode);
        }

        /// <summary>
        ///     Calls the event for the specified property.
        /// </summary>
        /// <param name="propName">Specified property name.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for raise property changed event.
        /// </param>
        [NotifyPropertyChangedInvocator("propName")]
        protected internal void OnPropertyChanged(string propName, ExecutionType executionMode)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propName), executionMode);
        }

        /// <summary>
        ///     Calls the event for the specified property.
        /// </summary>
        /// <param name="expression">Specified expression with property.</param>
        [NotifyPropertyChangedInvocator("expression")]
        protected void OnPropertyChanged<T>(Expression<Func<T>> expression)
        {
            OnPropertyChanged(expression, PropertyChangeExecutionMode);
        }

        /// <summary>
        ///     Calls the event for the specified property.
        /// </summary>
        /// <param name="expression">Specified expression with property.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for raise property changed event.
        /// </param>
        [NotifyPropertyChangedInvocator("expression")]
        protected void OnPropertyChanged<T>(Expression<Func<T>> expression, ExecutionType executionMode)
        {
            Should.NotBeNull(expression, "expression");
            string propName = MvvmUtilsInternal.ParsePropertyExpression(expression);
            OnPropertyChanged(propName, executionMode);
        }

        /// <summary>
        ///     Sets a property with calling property change event.
        /// </summary>
        /// <typeparam name="T">The type of property.</typeparam>
        /// <param name="propertyValue">The property value.</param>
        /// <param name="newValue">The new property value.</param>
        /// <param name="propName">Specified expression with property.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for raise property changed event.
        /// </param>
        [NotifyPropertyChangedInvocator("propName")]
        protected void SetProperty<T>(ref T propertyValue, T newValue, [CallerMemberName] string propName = "",
                                      ExecutionType? executionMode = null)
        {
            if (Equals(propertyValue, newValue)) return;
            propertyValue = newValue;
            if (executionMode == null)
                executionMode = PropertyChangeExecutionMode;
            OnPropertyChanged(propName, executionMode.Value);
        }

        /// <summary>
        ///     Sets a property with calling property change event.
        /// </summary>
        /// <typeparam name="T">The type of property.</typeparam>
        /// <param name="propertyValue">The property value.</param>
        /// <param name="newValue">The new property value.</param>
        /// <param name="expression">Specified expression with property.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for raise property changed event.
        /// </param>
        [NotifyPropertyChangedInvocator("expression")]
        protected void SetProperty<T>(ref T propertyValue, T newValue, Expression<Func<T>> expression,
                                      ExecutionType? executionMode = null)
        {
            if (Equals(propertyValue, newValue)) return;
            propertyValue = newValue;
            if (executionMode == null)
                executionMode = PropertyChangeExecutionMode;
            OnPropertyChanged(expression, executionMode.Value);
        }

        /// <summary>
        ///     Calls the event for the specified property.
        /// </summary>
        /// <param name="args">The specified property args.</param>
        /// <param name="executionMode">
        ///     Specifies the execution mode for raise property changed event.
        /// </param>
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs args, ExecutionType executionMode)
        {
            Should.NotBeNull(args, "args");
            if (IsNotificationsSuspended)
            {
                IsNotificationsDirty = true;
                return;
            }
            PropertyChangedEventHandler propertyChangedEventHandler = PropertyChanged;
            if (propertyChangedEventHandler != null)
                executionMode.Invoke(ThreadManager, () => propertyChangedEventHandler(this, args));
        }

        /// <summary>
        /// Performed during deserialization
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        protected virtual void OnDeserialized(StreamingContext context)
        {
        }

        [OnDeserialized]
        [EditorBrowsable(EditorBrowsableState.Never)]
#if WPF
        private void OnDeserializedCallback(StreamingContext context)
#else
        internal void OnDeserializedCallback(StreamingContext context)
#endif
        {
            PropertyChangeExecutionMode = GlobalSettings.PropertyChangeExecutionMode;
            OnDeserialized(context);
        }

        /// <summary>
        ///     Clears all <see cref="PropertyChanged" /> subscriber.
        /// </summary>
        protected void ClearPropertyChangedEvents()
        {
            var propertyChanged = PropertyChanged;
            if (propertyChanged == null) return;
            var invocationList = propertyChanged.GetInvocationList();
            foreach (PropertyChangedEventHandler eventHandler in invocationList)
                PropertyChanged -= eventHandler;
        }

        private void EndSuspendNotifications()
        {
            Interlocked.Decrement(ref _suspendCount);
            if (_suspendCount != 0) return;
            if (IsNotificationsDirty)
            {
                IsNotificationsDirty = false;
#if NETFX_CORE
                OnPropertyChanged(string.Empty);
#else
                OnPropertyChanged(null);
#endif
            }
            OnPropertyChanged("IsNotificationsSuspended");
        }

        #endregion

        #region Implementation of ISuspendNotifications

        /// <summary>
        ///     Gets or sets a value indicating whether change notifications are suspended. <c>True</c> if notifications are suspended, otherwise, <c>false</c>.
        /// </summary>
        public virtual bool IsNotificationsSuspended
        {
            get { return _suspendCount != 0; }
        }

        /// <summary>
        ///     Suspends the change notifications until the returned <see cref="IDisposable" /> is disposed.
        /// </summary>
        /// <returns>An instance of token.</returns>
        public virtual IDisposable SuspendNotifications()
        {
            Interlocked.Increment(ref _suspendCount);
            if (_suspendCount == 1)
                OnPropertyChanged("IsNotificationsSuspended");
            return new ActionToken(EndSuspendNotifications);
        }

        #endregion
    }
}